/*
 * $Id: NoidCommand.java 3 2007-12-07 01:24:30Z rasan.rasch $
 *
 * Copyright (c) 2002-2006 UC Regents
 * 
 * Permission to use, copy, modify, distribute, and sell this software and
 * its documentation for any purpose is hereby granted without fee, provided
 * that (i) the above copyright notices and this permission notice appear in
 * all copies of the software and related documentation, and (ii) the names
 * of the UC Regents and the University of California are not used in any
 * advertising or publicity relating to the software without the specific,
 * prior written permission of the University of California.
 * 
 * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
 * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
 * 
 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE FOR ANY
 * SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY
 * THEORY OF LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE
 * OR PERFORMANCE OF THIS SOFTWARE.
 */

package org.cdl.noid;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.regex.*;

/**
 *
 * @author  John Kunze (jak@ucop.edu)
 * @author  Rasan Rasch (rasan@nyu.edu)
 * @version $Revision: 3 $
 */
public class NoidCommand {

	private Noid noid;
	private boolean isWeb;
	private String[] command;

	private static HashMap<String,String> validCommands;

	private static final String[] commands = {
		"bind", "dbinfo", "dbcreate", "fetch",
		"get", "hello", "help", "hold", "mint",
		"note", "peppermint", "queue", "validate"
	};

	private static String[] validHelpTopics;

	public NoidCommand(String dbHome, String[] command) {
		this.isWeb = isWebContext();
		this.noid = new Noid(dbHome, getContact());
		this.command = command;
		init();
	}

	private boolean isWebContext() {
		return false;
	}

	private String getContact() {
		return "";
	}
		
	void init() {
		validCommands = new HashMap<String,String>();
		int len = commands.length;
		for (int i = 0; i < len; i++) {
			validCommands.put(commands[i], "1");
		}
	}
	
	
	private boolean runCommand() {
	
		String cmd = command[0];

		if (command.length > 0) {
			cmd = command[0];
		} else {
			usage(true, true, "intro");
			return false;
		}
	
		System.err.println("command = " + cmd);

// 		String dbFilePath = noid.getDbFilePath();
// 		File dbFile = new File(dbFilePath);
// 		if (!dbFile.isFile()
// 				&& !Util.matches("(dbcreate|help|hello|test)", cmd)) {
// 			Util.bprint(System.err, "error: no database (" + dbFilePath + ") "
// 				+ "-- use dbcreate?\n\n");
// 			usage(true, true, "intro");
// 			return false;
// 		}

		int argsLen = command.length - 1;
		String args[] = new String[argsLen];
		System.arraycopy(command, 1, args, 0, argsLen);

		if (!validCommands.containsKey(cmd)) {
			System.err.print("error: no such command: " + cmd
				+ " (" + Util.join(" ", args) + ")\n");
			usage(true, true, "intro");
			return false;
		}
		
		if (isWeb && command.equals("dbcreate")) {
			System.err.print("error: command \""
				+ command + "\" not allowed in URL interface.\n");
			usage(true, true, "intro");
			return false;
		}
		
		// Use reflection to call the appropriate method with args
		// from command line arguments.
		Object[] arguments;
		Class[] parameterTypes;
		Class c = NoidCommand.class;
		Method method;

		if (cmd.matches("^(fetch|get|validate)$")) {
			int arrayArgLen = args.length - 1;
			String firstArg = args[0];
			String arrayArg[] = new String[arrayArgLen];
			System.arraycopy(args, 1, arrayArg, 0, arrayArgLen);

			System.err.println("firstArg = " + firstArg);
			for (int i = 0; i < arrayArg.length; i++) {
				System.err.println("arrayArg[" + i + "] = " + arrayArg[i]);
			}

			arguments = new Object[] {firstArg, arrayArg};
			parameterTypes = new Class[] {String.class,
			                              arrayArg.getClass()};
		} else {
			System.err.println("Setting up methods call.");
			arguments = new Object[argsLen];
			parameterTypes = new Class[argsLen];
			for (int i = 1; i < argsLen; i++) {
				arguments[i] = args[i];
				parameterTypes[i] = String.class;
			}
		}

		try {
			method = c.getMethod(cmd, parameterTypes);
			method.invoke(this, arguments);
		} catch (NoSuchMethodException e) {
			System.err.println("No such command " + cmd + ". " + e);
		} catch (IllegalAccessException e) {
			System.err.println(e);
		} catch (InvocationTargetException e) {
			Exception f = (Exception)e.getTargetException();
			f.printStackTrace();
		}

		return true;
	}


	public boolean usage(boolean isError, boolean brief, String topic) {
		PrintStream out = isError ? System.err : System.out;
		if (topic == null)
			topic = "intro";
		topic = topic.toLowerCase();
		
		HashMap<String,String> info = null;
		
		if (true)
			info = initHelp();

		int k;

		ArrayList<String> blurbs = new ArrayList<String>();
		for (k = 0; k < validHelpTopics.length; k++) {
			if (validHelpTopics[k].startsWith(topic))
				blurbs.add(validHelpTopics[k]);
		}

		if (blurbs.size() != 1) {
			out.print(
				(blurbs.size() < 1
					? "Sorry: nothing under \"" + topic + "\".\n"
					: "Help: Your request (" + topic + "), "
						+ "matches more than one "
						+ "topic:\n\t(" + blurbs.toString() + ").\n")
				+ " You might try one of these topics:");

			ArrayList<String> topics = new ArrayList<String>();
			for (k = 0; k < validHelpTopics.length; k++)
				topics.add(validHelpTopics[k]);
			int n = 0;
			int numTopicsPerLine = 8;
			while (true) {
				if (topics.size() == 0) {
					System.out.println();
					break;
				} else if (n++ % numTopicsPerLine == 0) {
					System.out.print("\n\t");
				} else {
					System.out.print(" " + (String)topics.remove(0));
				}
			}
			System.out.print("\n\n");
			return true;
		}
	
		String blurb = (String)blurbs.remove(0);
	
		String t;
		String i;
		if (blurb.equals("intro")) {
			Util.bprint(out, "\n"
				+ "Usage:\n"
				+ "              noid [-f dbhome] [-v] [-h] Command Arguments"
				+ (brief
					? "\n"
						+ "              noid -h             (for "
						+ "help with a Command summary)."
					: "\n\n"
						+ "\n"
						+ "Dbhome defaults to \".\" if not "
						+ "found from -f or a NOID environment variable.\n"
						+ "For more information try \"perldoc noid\" "
						+ "or \"noid help Command\".  Summary:\n"));
	
			if (brief) {
				System.out.print("\n\n");
				return true;
			}
	
			for (k = 0; k < commands.length; k++) {
				t = commands[k];
				i = (String)info.get(t + "/brief");
				if (i == null || i.length() == 0)
					continue;
				Util.bprint(out, i);
			}
	
				
			Util.bprint(out, "\n"
				+ "If invoked as \"noidu...\", output is formatted for a web client.  Give Command\n"
				+ "as \"-\" to run a block of noid Commands read from stdin or from POST data.@;\n");
			System.out.print("\n\n");
			return true;
		}
		
		if (brief)
			blurb += "/brief";
		t = (String)info.get(blurb);
		
		if (t == null || t.length() == 0) {
			out.print("Sorry: no information on \"" + blurb + "\".\n\n");
			return true;
		}
		Util.bprint(out, t);
		System.out.println();
	
		String yyyy = "\n"
			+ "Called as \"noid\", an id generator accompanies every COMMAND.  Called as\n"
			+ "\"noi\", the id generator is supplied implicitly by looking first for a\n"
			+ "NOID environment variable and, failing that, for a file calld \".noid\" in\n"
			+ "the current directory.  Examples show the explicit form.  To create a\n"
			+ "generator, use\n"
			+ "\n"
			+ "	noid ck8 dbcreate TPL SNAA\n"
			+ "\n"
			+ "where you replace TPL with a template that defines the shape and number\n"
			+ "of all identifiers to be minted by this generator.  You replace SNAA with\n"
			+ "the name (eg, the initials) of the sub NAA (Name Assigning Authority) that\n"
			+ "will be responsible for this generator; for example, if the Online Archive\n"
			+ "of California is the sub-authority for a template, SNAA could be \"oac\".\n"
			+ "This example of generator intialization,\n"
			+ "\n"
			+ "	noid oac.noid dbcreate pd2.wwdwwdc oac\n"
			+ "\n"
			+ "sets up the \"oac.noid\" identifier generator.  It can create \"nice opaque\n"
			+ "identifiers\", such as \"pd2pq5dk9z\", suitable for use as persistent\n"
			+ "identifiers should the supporting organization wish to provide such a\n"
			+ "level of commitment.  This generator is also capable of holding a simple\n"
			+ "sequential counter (starting with 1), which some callers may wish to use\n"
			+ "as an internal number to keep track of minted external identifiers.\n"
			+ "[ currently accessible only via the count() routine ]\n"
			+ "\n"
			+ "In the example template, \"pd2\" is a constant prefix for an identifier\n"
			+ "generator capable of producing 70,728,100 identifiers before it runs out.\n"
			+ "A template has the form \"prefix.mask\", where 'prefix' is a literal string\n"
			+ "prepended to each identifier and 'mask' specifies the form of the generated\n"
			+ "identifier that will appear after the prefix (but with no '.' between).\n"
			+ "Mask characters are 'd' (decimal digit), 'w' (limited alpha-numeric\n"
			+ "digit), 'c' (a generated check character that may only appear in the\n"
			+ "terminal position).\n"
			+ "\n"
			+ "Alternatively, if the mask contains an 's' (and no other letters), dbcreate\n"
			+ "initializes a generator of sequential numbers.  Instead of seemingly random\n"
			+ "creates sequentially generated number.  Use '0s'\n"
			+ "to indicate a constant width number padded on the left with zeroes.\n";
		return true;
	}


	private HashMap<String,String> initHelp() {

		String[] foo = { "intro", "all", "template" };
		validHelpTopics = new String[commands.length + 3];
		System.arraycopy(foo, 0, validHelpTopics, 0, foo.length);
		System.arraycopy(commands, 0, validHelpTopics, foo.length, commands.length);
	
		HashMap<String,String> info = new HashMap<String,String>();
	
		info.put("bind/brief", "\n"
			+ "   noid bind How Id Element Value	# to bind an Id's Element, where\n"
			+ "      How is set|add|insert|new|replace|mint|append|prepend|delete|purge.\n"
			+ "      Use an Id of :idmap/Idpattern, Value=PerlReplacementPattern so that\n"
			+ "      fetch returns variable values.  Use \":\" as Element to read Elements\n"
			+ "      and Values up to a blank line from stdin (up to EOF with \":-\").\n");
		info.put("bind", "");
		info.put("dbinfo/brief", "");
		info.put("dbinfo", "");
		info.put("dbcreate/brief", "\n"
			+ "   noid dbcreate [ Template (long|-|short) [ NAAN NAA SubNAA ] ]\n"
			+ "      where Template=prefix.Tmask, T=(r|s|z), and mask=string of (e|d|k)\n");
		info.put("dbcreate", "\n"
			+ "To create an identifier minter governed by Template and Term (\"long\" or \"-\"),\n"
			+ "\n"
			+ "   noid dbcreate [ Template Term [ NAAN NAA SubNAA ] ]\n"
			+ "\n"
			+ "The Template gives the number and form of generated identifiers.  Examples:\n"
			+ "\n"
			+ "    .rddd        minter of random 3-digit numbers that stops after the 1000th\n"
			+ "    .zd          sequential numbers without limit, adding new digits as needed\n"
			+ "  bc.sdddd       sequential 4-digit numbers with constant prefix \"bc\"\n"
			+ "    .rdedeede    .7 billion random ids, extended-digits at chars 2, 4, 5 and 7\n"
			+ "  fk.rdeeek      .24 million random ids with prefix \"fk\" and final check char\n"
			+ "\n"
			+ "For persistent identifiers, use \"long\" for Term, and specify the NAAN, NAA,\n"
			+ "and SubNAA.  Otherwise, use \"-\" for Term or omit it.  The NAAN is a globally\n"
			+ "registered Name Assigning Authority Number; for identifiers conforming to the\n"
			+ "ARK scheme, this is a 5-digit number registered with ark@cdlib.org, or 00000.\n"
			+ "The NAA is the character string equivalent registered for the NAAN; for\n"
			+ "example, the NAAN, 13030, corresponds to the NAA, \"cdlib.org\".  The SubNAA\n"
			+ "is also a character string, but it is a locally determined and possibly\n"
			+ "structured subauthority string (e.g., \"oac\", \"ucb/dpg\", \"practice_area\") that\n"
			+ "is not globablly registered.\n");
		info.put("fetch/brief", "\n"
			+ "   noid fetch Id Element ...		# fetch/map one or more Elements\n");
		info.put("fetch", "\n"
			+ "To bind,\n"
			+ "\n"
			+ "   noid bind replace fk0wqkb myGoto http://www.cdlib.org/foobar.html\n"
			+ "\n"
			+ "sets \"myGoto\" element of identifier \"fk0wqkb\" to a string (here a URL).\n");
		info.put("get/brief", "\n"
			+ "   noid get Id Element ...		# fetch/map Elements without labels\n");
		info.put("get", "");
		info.put("hello/brief", "");
		info.put("hello", "");
		info.put("hold/brief", "\n"
			+ "   noid hold (set|release) Id ...	# place or remove a \"hold\" on Id(s)\n");
		info.put("hold", "");
		info.put("mint/brief", "\n"
			+ "   noid mint N [ Elem Value ]	# to mint N identifiers (optionally binding)\n");
		info.put("mint", "");
		info.put("note/brief", "");
		info.put("note", "");
		info.put("peppermint/brief", "");
		info.put("peppermint", "");
		info.put("queue/brief", "\n"
			+ "   noid queue (now|first|lvf|Time) Id ...	# queue (eg, recycle) Id(s)\n"
			+ "      Time is NU, meaning N units, where U= d(ays) | s(econds).\n"
			+ "      With \"lvf\" (Lowest Value First) lowest value of id will mint first.\n");
		info.put("queue", "");
		info.put("validate/brief", "\n"
			+ "   noid validate Template Id ...	# to check if Ids are valid\n"
			+ "      Use Template of \"-\" to use the minter's native template.\n");
		info.put("validate", "");
		
		return info;
	}
	

	private boolean dbcreate (String templ,
			String policy, String naan,
			String naa, String subnaa) {

		String report = noid.dbCreate(templ, policy, naan, naa, subnaa);

		if (report == null) {
			System.err.print(noid.getMsg());
			return false;
		}

		System.out.println(report);
		return true;
	}


	private boolean bind(String how, String id,
							String elem, String value) {

		String report;
		boolean validate = true;
		boolean isOpen = noid.open();

		if (!isOpen) {
			System.err.println(noid.getMsg());
			return false;
		}

		if (elem == null) {
			elem = "";
		}
		if (elem.equals(":")) {
			if (value != null) {
				System.err.println("Why give a value (" + value
					+ ") with an element \"" + elem + "\"?");
				noid.close();
				return false;
			}

			Pattern patt = Pattern.compile("^\\s+");
			Matcher matcher = patt.matcher("");

			StringBuffer sb = new StringBuffer();
			String line = null;
			int lineNo = 0;
			BufferedReader in =
				new BufferedReader(new InputStreamReader(System.in));
			try {
				while ((line = in.readLine()) != null && line.length() > 0) {
					if (line.startsWith("#")) {
						continue;
					}
					line.replaceFirst("\\s+$", "");
					matcher.reset(line);
					if (matcher.find()) {
						line = matcher.replaceFirst(" ");
						if (lineNo > 0) {
							sb.deleteCharAt(sb.length() - 1);
						}
					}
					sb.append(line + "\n");
					lineNo++;
				}
			} catch (IOException e) {
				e.printStackTrace();
			}

			String paragraph = sb.toString();
			String[] lines = paragraph.split("\n");
	
			int bound = 0;
			int total = 0;

			patt = Pattern.compile("^([^:]+)\\s*:\\s*(.*)");
			matcher = patt.matcher("");
			
			for (int i = 0; i < lines.length; i++) {
				matcher.reset(lines[i]);
				if (!matcher.find()) {
					break;
				}
				total++;
				elem = matcher.group(1);
				value = matcher.group(2);

				if (elem == null) {
					noid.addMsg("error: " + id + ": bad element associated "
						+ "with value \"" + value + "\".");
					break;
				}

				if (value == null) {
					value = "";
				}
			
				report = noid.bind(validate, how, id, elem, value);
				if (report == null) {
					usage(true, true, "bind");
				} else {
					bound++;
					System.out.println(report);
				}

			}

			noid.close();

		} else if (elem.equals(":-")) {

			if (value != null) {
				System.err.println("Why give a value (" + value
					+ ") with an element \"" + elem + "\"?");
				noid.close();
				return false;
			}
			
			ArrayList<String> lines = new ArrayList<String>();
			String line = null;
			BufferedReader in =
				new BufferedReader(new InputStreamReader(System.in));
			
			try {
				while ((line = in.readLine()) != null) {
					if (line.startsWith("#")
							|| line.matches("^\\s*$")) {
						continue;
					}
					lines.add(line);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}

			if (lines.size() == 0) {
				System.err.println("error:  no non-blank, "
					+ "non-comment input.");
				noid.close();
				return false;
			}

			Pattern patt = Pattern.compile("^\\s*(\\w+)\\s*:\\s*(.*)$");
			Matcher matcher = patt.matcher((String) lines.get(0));

			if (!matcher.find()) {
				System.err.println("error:  missing element or colon on "
					+ "first non-blank, non-comment line.");
				noid.close();
				return false;
			}

			elem = matcher.group(1);
			value = matcher.group(2);

			for (int i = 1; i < lines.size(); i++) {
				value += "\n" + (String) lines.get(i);
			}


		} else if (elem.startsWith(":")) {
			System.err.println("Binding to element syntax \"" + elem
				+ "\" not supported");
			noid.close();
			return false;
		}

		report = noid.bind(validate, how, id, elem, value);
		boolean success = report != null;
		if (success) {
			System.out.println(report);
		} else {
			usage(false, true, "bind");
		}
		noid.close();
		return success;
	}


	private boolean dbinfo(String level) {
		if (level == null) {
			level = "brief";
		}

		boolean isOpen = noid.open(true);

		if (!isOpen) {
			System.err.println(noid.getMsg());
			return false;
		}

		noid.dbInfo(level);
		noid.close();
		
		return true;
	}
	
	private boolean fetch(String id, String[] elems) {
		return getValue(true, id, elems);
	}
	
	private boolean get(String id, String[] elems) {
		return getValue(false, id, elems);
	}
	
	private boolean getValue(boolean verbose, String id, String[] elems) {

		boolean isOpen = noid.open(true);

		if (!isOpen) {
			System.err.println(noid.getMsg());
			return false;
		}

		String fetched = noid.fetch(verbose, id, elems);
		if (fetched == null) {
			System.err.println(noid.getMsg());
		} else {
			System.out.print(fetched);
			if (verbose) {
				System.out.println();
			}
		}
		noid.close();
		return true;
	}
	

	private void hello() {
		System.out.println("Hello.");
	}
	
	private void showMethods() {
		Class c = NoidCommand.class;
		Method[] theMethods = c.getMethods();
		for (int i = 0; i < theMethods.length; i++) {
			String methodString = theMethods[i].getName();
			System.out.println("Name: " + methodString);
			String returnString =
				theMethods[i].getReturnType().getName();
			System.out.println("   Return Type: " + returnString);
			Class[] parameterTypes = theMethods[i].getParameterTypes();
			System.out.print("   Parameter Types:");
			for (int k = 0; k < parameterTypes.length; k ++) {
				String parameterString = parameterTypes[k].getName();
				System.out.print(" " + parameterString);
			}
			System.out.println();
		}
	}


	private boolean help (String topic) {
		return usage(false, false, topic);
	}
	
	private boolean hold(String action, String[] ids) {
	
		boolean isOpen = noid.open();
		if (!isOpen) {
			System.err.println(noid.getMsg());
			return false;
		}
		
		boolean ok = noid.hold(action, ids);
		
		if (!ok) {
			System.err.print(noid.getMsg());
			usage(true, true, "hold");
		}
		
		return ok;
	}

	private boolean peppermint(String num,
			String elem, String value) {
		return mint(num, elem, value, true);
	}

	private boolean mint(String num) {
		return mint(num, null, null, false);
	}

	private boolean mint (String num, String elem,
			String value, boolean pepper) {

		if (pepper) {
			System.err.println(
				"The peppermint command is not implemented yet.");
			return false;
		}

		if (num == null || !num.matches("^\\d+$")) {
			System.err.println("Argument error: expected "
				+ "positive integer, got "
				+ (num != null ? "\"" + num + "\"" : "nothing"));
			usage(true, true, "mint");
			return false;
		}
		
		int n = Integer.parseInt(num);
		
		boolean isOpen = noid.open();
		if (!isOpen) {
			System.err.println(noid.getMsg());
			return false;
		}
	
		String id;
		while (n-- > 0) {
			id = noid.mint(pepper);
			if (id == null) {
				System.err.print(noid.getMsg());
				noid.close();
				return false;
			}
			System.out.println("id: " + id);
			System.err.println("ID: " + id);
		}
		
		noid.close();
		System.out.println();
		return true;
	}
	
	private boolean note(String key, String value) {
		if (key == null || value == null) {
			System.err.println("You must supply a key and a value.");
			usage(true, true, "note");
			return false;
		}
		noid.open();
		if (!noid.note(key, value))
			System.err.println(noid.getMsg());
		noid.close();
		return true;
	}

	private boolean queue (String when, String[] ids) {
	
		boolean isOpen = noid.open();

		if (!isOpen) {
			System.err.println(noid.getMsg());
			return false;
		}

		String[] queued = noid.queue(when, ids);
		boolean success = queued.length > 0;
		if (success) {
			System.out.println(Util.join("\n", queued));
		} else {
			System.err.println(noid.getMsg());
		}

		int n = 0;
		for (int i = 0; i < queued.length; i++) {
			if (!queued[i].startsWith("error:")) {
				n++;
			}
		}
		
		System.out.println("note: " + n + " identifier"
			+ (n == 1 ? "" : "s") + " queued");
		noid.close();
		return success;
	}


	private int validate(String templ, String[] ids) {

		boolean isOpen = noid.open(true);

		if (!isOpen) {
			System.err.println(noid.getMsg());
			return 0;
		}

		String[] valids = noid.validate(templ, ids);
		if (valids.length == 0) {
			System.err.println(noid.getMsg());
			noid.close();
			usage(true, true, "validate");
			return 0;
		}

		int numErrs = 0;
		for (int i = 0; i < valids.length; i++) {
			System.out.println(valids[i]);
			if (valids[i].startsWith("error:")) {
				numErrs++;
			}
		}

		noid.close();
		return ids.length - numErrs;
	}

}

// vim: set ts=4 nohls:
